/****

    activequant - activestocks.eu

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

	
	contact  : contact@activestocks.eu
    homepage : http://www.activestocks.eu

 ****/
package org.activequant.calendar;

import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.TimeZone;

import org.activequant.core.types.TimeStamp;
import org.activequant.core.util.Check;
import org.activequant.util.calendar.CalendarUtils;

/**
 * A holiday calendar entry.<br>
 * <br>
 * 
 * This is a calendar entry for different types of holidays.<br>
 * <br>
 * 1) Recurring-date holidays: yearly repeating holidays which will be on the same date every year. For these kind of
 * holidays the as-of-year field marks the beginning of the yearly repeating holiday calendar entry and the until-year
 * field will not be set, indicating it will forever recur every year.<br>
 * 2) Varying-date holidays: holidays that can be on different dates in subsequent years. These kind of holidays will
 * have the as-of-year field set to mark the first year of the holiday and the until-year field will be set to mark the
 * last year of the holiday on that particular date.<br>
 * 3) All-day holidays: these will last the hole day, so the start and end time fields will not be set.<br>
 * 4) Partial-day holidays: these holidays do not last a hole day so the start and end time fields will be set to mark
 * the start and end time of the holiday.<br>
 * <br>
 * Combinations of type 1 and 2 with type 3 or 4 are possible.<br>
 * <br>
 * HolidayCalendarEntry implements ICalendarEntry. Holds the following associated variables:
 * <ul>
 * <li>dayOfMonth(int)</li>
 * <li>monthOfYear(int)</li>
 * <li>asOfYear(int)</li>
 * <li>untilYear(Integer)</li>
 * <li>startTimeInMillisAfterMidnight(Long)</li>
 * <li>endTimeInMillisAfterMidnight(Long)</li>
 * <li>timeZone(TimeZone)</li>
 * </ul>
 * <b>History:</b><br>
 * - [May 31, 2008] Created (mark)<br>
 * - [Aug 22, 2008] added different types of holiday calendar entries (mark)<br>
 * 
 * @author mark
 */
public class HolidayCalendarEntry implements ICalendarEntry {

	/**
	 * private final int dayOfMonth;<br/>
	 * starts with 1, see Calendar.DAY_OF_MONTH
	 */
	private final int dayOfMonth;

	/**
	 * private final int monthOfYear;<br/>
	 * starts with 0, see Calendar.JANUARY
	 */
	private final int monthOfYear;

	/**
	 * private final int asOfYear;<br/>
	 * the year as of which this holiday is in effect
	 */
	private final int asOfYear;

	/**
	 * private Integer untilYear;<br/>
	 * the year until this holiday is in effect. Only used for varying holidays, so can be null.
	 */
	private Integer untilYear;

	/*
	 * the start and end time (in milliseconds after midnight, local time) used only for partial-day holiday calendar
	 * entries, so can be null.
	 */
	/**
	 * private Long startTimeInMillisAfterMidnight;<br/>
	 * the start time (in milliseconds after midnight, local time) used only for partial-day holiday calendar entries, so can be null.
	 */
	private Long startTimeInMillisAfterMidnight;
	/**
	 * private Long endTimeInMillisAfterMidnight;<br/>
	 * the end time (in milliseconds after midnight, local time) used only for partial-day holiday calendar entries, so can be null.
	 */
	private Long endTimeInMillisAfterMidnight;
	/**
	 * private final TimeZone timeZone;
	 */
	private final TimeZone timeZone;

	/**
	 * Create a new all-day, recurring-date holiday calendar entry.<br/>
	 * constructs a HolidayCalendarEntry (implements ICalendarEntry) using the given asOfYear(int), monthOfYear(int), dayOfMonth(int) and timeZone(TimeZone) to set its
	 * associated asOfYear(int), monthOfYear(int), dayOfMonth(int) and timeZone(TimeZone).<br/>
	 * It also validates that the given dayOfMonth(int) is in the 1-31 range, and monthOfYear(int) is in the 0-11 range.
	 * @param asOfYear
	 *            the year as of which this holiday calendar entry is in effect
	 * @param monthOfYear
	 *            the month of the year for the holiday calendar entry
	 * @param dayOfMonth
	 *            the day of the month for the holiday calendar entry
	 * @param timeZone
	 *            the time zone for this holiday calendar entry.
	 * 
	 */
	public HolidayCalendarEntry(final int asOfYear, final int monthOfYear, final int dayOfMonth, final TimeZone timeZone) {
		Check.atLeast("dayOfMonth", 1, dayOfMonth);
		Check.atMost("dayOfMonth", 31, dayOfMonth);

		Check.atLeast("monthOfYear", 0, monthOfYear);
		Check.atMost("monthOfYear", 11, monthOfYear);

		Check.notNull("timeZone", timeZone);

		this.asOfYear = asOfYear;
		this.dayOfMonth = dayOfMonth;
		this.monthOfYear = monthOfYear;
		this.timeZone = timeZone;
	}

	/**
	 * Create a new all-day, varying-date holiday calendar entry.<br/>
	 * constructs a HolidayCalendarEntry (implements ICalendarEntry) using the given asOfYear(int), untilYear(int), monthOfYear(int), dayOfMonth(int) and timeZone(TimeZone) to set its
	 * associated asOfYear(int), untilYear(Integer), monthOfYear(int), dayOfMonth(int) and timeZone(TimeZone).<br/>
	 * It also validates that the given dayOfMonth(int) is in the 1-31 range, and monthOfYear(int) is in the 0-11 range. The given untilYear(int) can't be less than the given asOfYear(int).
	 * @param asOfYear
	 *            the year as of which this holiday calendar entry is in effect
	 * @param untilYear
	 *            the year until this holiday calendar entry is in effect
	 * @param monthOfYear
	 *            the month of the year for the holiday calendar entry
	 * @param dayOfMonth
	 *            the day of the month for the holiday calendar entry
	 * @param timeZone
	 *            the time zone for this holiday calendar entry.
	 * 
	 */
	public HolidayCalendarEntry(final int asOfYear, final int untilYear, final int monthOfYear, final int dayOfMonth, final TimeZone timeZone) {
		this(asOfYear, monthOfYear, dayOfMonth, timeZone);

		Check.atLeast("untilYear", asOfYear, untilYear);
		this.untilYear = new Integer(untilYear);
	}

	/**
	 * Create a new partial-day, recurring-date holiday calendar entry.<br/>
	 * constructs a HolidayCalendarEntry (implements ICalendarEntry) using the given asOfYear(int), monthOfYear(int), dayOfMonth(int), startTimeInMillisAfterMidnight(long), endTimeInMillisAfterMidnight(long) and timeZone(TimeZone) to set its
	 * associated asOfYear(int), monthOfYear(int), dayOfMonth(int), startTimeInMillisAfterMidnight(Long), endTimeInMillisAfterMidnight(Long) and timeZone(TimeZone).<br/>
	 * It also validates that the given dayOfMonth(int) is in the 1-31 range, and monthOfYear(int) is in the 0-11 range.<br/>
	 * The given startTimeInMillisAfterMidnight(long) has to be 0 or greater and the endTimeInMillisAfterMidnight(long) can't be less than that.
	 * @param asOfYear
	 *            the year as of which this holiday calendar entry is in effect
	 * @param monthOfYear
	 *            the month of the year for the holiday calendar entry
	 * @param dayOfMonth
	 *            the day of the month for the holiday calendar entry
	 * @param startTimeInMillisAfterMidnight
	 *            the time this holiday will start in milliseconds after midnight (local time)
	 * @param endTimeInMillisAfterMidnight
	 *            the time this holiday will end in milliseconds after midnight (local time)
	 * @param timeZone
	 *            the time zone for this holiday calendar entry.
	 */
	public HolidayCalendarEntry(final int asOfYear, final int monthOfYear, final int dayOfMonth, final long startTimeInMillisAfterMidnight, final long endTimeInMillisAfterMidnight, final TimeZone timeZone) {
		this(asOfYear, monthOfYear, dayOfMonth, timeZone);

		Check.atLeast("startTimeInMillisAfterMidnight", 0, startTimeInMillisAfterMidnight);
		Check.atLeast("endTimeInMillisAfterMidnight", startTimeInMillisAfterMidnight, endTimeInMillisAfterMidnight);

		this.startTimeInMillisAfterMidnight = new Long(startTimeInMillisAfterMidnight);
		this.endTimeInMillisAfterMidnight = new Long(endTimeInMillisAfterMidnight);
	}

	/**
	 * Create a new partial-day, varying-date holiday calendar entry.<br/>
	 * constructs a HolidayCalendarEntry (implements ICalendarEntry) using the given asOfYear(int), untilYear(int), monthOfYear(int), dayOfMonth(int), startTimeInMillisAfterMidnight(long), endTimeInMillisAfterMidnight(long) and timeZone(TimeZone) to set its
	 * associated asOfYear(int), untilYear(Integer), monthOfYear(int), dayOfMonth(int), startTimeInMillisAfterMidnight(Long), endTimeInMillisAfterMidnight(Long) and timeZone(TimeZone).<br/>
	 * It also validates that the given dayOfMonth(int) is in the 1-31 range, and monthOfYear(int) is in the 0-11 range.<br/>
	 * The given startTimeInMillisAfterMidnight(long) has to be 0 or greater and the endTimeInMillisAfterMidnight(long) can't be less than that.<br/>
	 * The given untilYear(int) can't be less than the asOfYear(int).
	 * @param asOfYear
	 *            the year as of which this holiday calendar entry is in effect
	 * @param untilYear
	 *            the year until this holiday calendar entry is in effect
	 * @param monthOfYear
	 *            the month of the year for the holiday calendar entry
	 * @param dayOfMonth
	 *            the day of the month for the holiday calendar entry
	 * @param startTimeInMillisAfterMidnight
	 *            the time this holiday will start in milliseconds after midnight (local time)
	 * @param endTimeInMillisAfterMidnight
	 *            the time this holiday will end in milliseconds after midnight (local time)
	 * @param timeZone
	 *            the time zone for this holiday calendar entry.
	 */
	public HolidayCalendarEntry(final int asOfYear, final int untilYear, final int monthOfYear, final int dayOfMonth, final long startTimeInMillisAfterMidnight, final long endTimeInMillisAfterMidnight, final TimeZone timeZone) {
		this(asOfYear, monthOfYear, dayOfMonth, startTimeInMillisAfterMidnight, endTimeInMillisAfterMidnight, timeZone);
		Check.atLeast("untilYear", asOfYear, untilYear);
		this.untilYear = new Integer(untilYear);
	}

	/**
	 * Gets the day of the month for this holiday calendar entry.<br/>
	 * returns the associated dayOfMonth(int)
	 * @return the day of the month for this holiday calendar entry (in range: 1-31).
	 */
	public int getDayOfMonth() {
		return this.dayOfMonth;
	}

	/**
	 * Gets the month of the year for this holiday calendar entry.<br/>
	 * returns the associated monthOfYear(int)
	 * @return the month of the year for this holiday calendar entry (in range: 0-11).
	 */
	public int getMonthOfYear() {
		return this.monthOfYear;
	}

	/**
	 * Gets the year which marks the beginning of this holiday calendar entry.<br/>
	 * returns the associated asOfYear(int)
	 * @return the year that marks the beginning of this holiday calendar entry.
	 */
	public int getAsOfYear() {
		return this.asOfYear;
	}

	/**
	 * Gets the year which marks the last year of this varying-date holiday calendar entry.<br/>
	 * returns the associated untilYear(Integer)
	 * @return the last year of this holiday calendar entry, or null if this is not a varying-date holiday entry.
	 */
	public Integer getUntilYear() {
		return this.untilYear;
	}

	/**
	 * Get the start time of the holiday in milliseconds after midnight (local time).<br/>
	 * returns the associated startTimeInMillisAfterMidnight(Long)
	 * @return the startTimeInMillisAfterMidnight or <code>null</code> if this is not a partial-day holiday calendar
	 *         entry.
	 */
	public Long getStartTimeInMillisAfterMidnight() {
		return this.startTimeInMillisAfterMidnight;
	}

	/**
	 * Get the end time of the holiday in milliseconds after midnight (local time).<br/>
	 * returns the associated endTimeInMillisAfterMidnight(Long)
	 * @return the endTimeInMillisAfterMidnight or <code>null</code> if this is not a partial-day holiday calendar
	 *         entry.
	 */
	public Long getEndTimeInMillisAfterMidnight() {
		return this.endTimeInMillisAfterMidnight;
	}
	/**
	 * returns the number of milliseconds since the Epoch represented by the associated timeZone(TimeZone), asOfYear(int), monthOfYear(int), dayOfMonth(int) and (if set) startTimeInMillisAfterMidnight(Long).
	 */
	public long getStartOfEntryAsMillis() {
		Calendar calendar = GregorianCalendar.getInstance(this.timeZone);
		CalendarUtils.clearAndSet(calendar, this.asOfYear, this.monthOfYear, this.dayOfMonth);

		if (isPartialDayHoliday()) {
			CalendarUtils.setTime(calendar, this.startTimeInMillisAfterMidnight);
		}

		return calendar.getTimeInMillis();
	}

	/**
	 * For recurring-date holiday calendar entries 0 will be returned indicating that the end date/time for this type of holiday entry is unknown.<br/>
	 * If the associated untilYear(Integer) is not null, it returns the number of milliseconds since the Epoch represented by the associated timeZone(TimeZone), untilYear(Integer), monthOfYear(int), dayOfMonth(int) and (if set) endTimeInMillisAfterMidnight(Long).
	 * Otherwise, it returns 0.
	 * See also {@link ICalendarEntry#getEndOfEntryAsMillis()}<br>
	 */
	public long getEndOfEntryAsMillis() {
		if (isVaryingDateHoliday()) {
			Calendar calendar = GregorianCalendar.getInstance(this.timeZone);
			CalendarUtils.clearAndSet(calendar, this.untilYear, this.monthOfYear, this.dayOfMonth);

			if (isPartialDayHoliday()) {
				CalendarUtils.setTime(calendar, this.endTimeInMillisAfterMidnight);
			}

			return calendar.getTimeInMillis();
		}

		return 0;
	}
	/**
	 * returns a String representation of this HolidayCalendarEntry in the form:<br/>
	 * "Holiday from: ${asOfYear}-${monthOfYear}-${dayOfMonth}${startTimeInMillisAfterMidnight}( to: ${untilYear}-${monthOfYear}-${dayOfMonth})${endTimeInMillisAfterMidnight}<br/>
	 * , where startTimeInMillisAfterMidnight and endTimeInMillisAfterMidnight are optional and in the form: "T ${HOUR_OF_DAY}:${MINUTE}:${SECOND} ${MILLISECOND} ${TimeZone}"
	 */
	@Override
	public String toString() {
		final StringBuilder builder = new StringBuilder();

		builder.append("Holiday from: " + new Integer(this.asOfYear).toString() + "-" + new Integer(this.monthOfYear).toString() + "-" + new Integer(this.dayOfMonth).toString());

		if (this.startTimeInMillisAfterMidnight != null) {
			builder.append(CalendarUtils.timeInMillisAfterMidnightToString(this.startTimeInMillisAfterMidnight, TimeZone.getDefault()));
		}

		if (this.untilYear != null) {
			builder.append(" to: " + this.untilYear + "-" + new Integer(this.monthOfYear).toString() + "-" + new Integer(this.dayOfMonth).toString());
		}
		if (this.endTimeInMillisAfterMidnight != null) {
			builder.append(CalendarUtils.timeInMillisAfterMidnightToString(this.endTimeInMillisAfterMidnight, TimeZone.getDefault()));
		}

		return builder.toString();
	}

	/**
	 * Determine if a time stamp is on the same day as this holiday calendar entry's day. No time part is checked.<br/>
	 * returns whether the DAY_OF_MONTH and MONTH represented (in the associated timeZone(TimeZone)) by the given now(TimeStamp) match
	 * the associated dayOfMonth(int) and monthOfYear(int). Also the YEAR must be within the associated asOfYear(int) - untilYear(Integer)(if set) range.
	 * @param now
	 *            the time stamp to check.
	 * @return true if the time stamp is on this holiday calendar entry's day, otherwise false.
	 */
	public boolean isOnSameDay(TimeStamp now) {
		final Calendar calendar = Calendar.getInstance(this.timeZone);
		calendar.setTimeInMillis(now.getDate().getTime());

		final int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);
		final int monthOfYear = calendar.get(Calendar.MONTH);
		final int year = calendar.get(Calendar.YEAR);

		if (isVaryingDateHoliday()) {
			return dayOfMonth == this.dayOfMonth && monthOfYear == this.monthOfYear && year >= this.asOfYear && year <= this.untilYear.intValue();
		} else {
			return dayOfMonth == this.dayOfMonth && monthOfYear == this.monthOfYear && year >= this.asOfYear;
		}
	}

	/**
	 * Determine if a time stamp coincides with this holiday calendar entry. For partial-day holidays the time part of
	 * the time stamp is also compared to the time part of the partial-day holiday calendar entry.<br/>
	 * returns whether the DAY_OF_MONTH and MONTH represented (in the associated timeZone(TimeZone)) by the given now(TimeStamp) match
	 * the associated dayOfMonth(int) and monthOfYear(int). Also the YEAR must be within the associated asOfYear(int) - untilYear(Integer)(if set) range.<br/>
	 * If the associated startTimeInMillisAfterMidnight(Long) is not null, it also checks whether the time part (in milliseconds) is in the associated 
	 * startTimeInMillisAfterMidnight(Long)-endTimeInMillisAfterMidnight(Long) range.
	 * @param now
	 *            the time stamp to check.
	 * @return true if the time stamp coincides with this holiday calendar entry's time part, otherwise false.
	 */
	public boolean isDuring(final TimeStamp now) {
		final Calendar calendar = Calendar.getInstance(this.timeZone);
		calendar.setTimeInMillis(now.getDate().getTime());
		if (isPartialDayHoliday()) {
			final int hourOfDay = calendar.get(Calendar.HOUR_OF_DAY);
			final int minutes = calendar.get(Calendar.MINUTE);
			final int seconds = calendar.get(Calendar.SECOND);
			final int millis = calendar.get(Calendar.MILLISECOND);
			long timeOfNow = (hourOfDay * CalendarUtils.HOUR_TO_MILLIS) + (minutes * CalendarUtils.MINUTES_TO_MILLIS) + (seconds * CalendarUtils.SECOND_TO_MILLIS) + millis;
			return isOnSameDay(now) && timeOfNow >= this.startTimeInMillisAfterMidnight && timeOfNow <= this.endTimeInMillisAfterMidnight;
		}

		return isOnSameDay(now);
	}

	/**
	 * returns whether the associated startTimeInMillisAfterMidnight(Long) is not null
	 * @return true if this is a partial-day holiday, otherwise false.
	 */
	public boolean isPartialDayHoliday() {
		return this.startTimeInMillisAfterMidnight != null;
	}

	/**
	 * returns whether the associated startTimeInMillisAfterMidnight(Long) is null
	 * @return true if this is a all-day holiday, otherwise false.
	 */
	public boolean isAllDayHoliday() {
		return this.startTimeInMillisAfterMidnight == null;
	}

	/**
	 * returns whether the associated untilYear(Integer) is not null
	 * @return true if this is a varying-date holiday, otherwise false.
	 */
	public boolean isVaryingDateHoliday() {
		return this.untilYear != null;
	}

	/**
	 * returns whether the associated untilYear(Integer) is null
	 * @return true if this is a recurring-date holiday, otherwise false.
	 */
	public boolean isRecurringDateHoliday() {
		return this.untilYear == null;
	}

	/**
	 * Get the time zone from this holiday calendar entry.<br/>
	 * returns the associated timeZone(TimeZone)
	 * @return the time zone.
	 */
	public TimeZone getTimeZone() {
		return this.timeZone;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + this.asOfYear;
		result = prime * result + this.dayOfMonth;
		result = prime * result
				+ ((this.endTimeInMillisAfterMidnight == null) ? 0 : this.endTimeInMillisAfterMidnight.hashCode());
		result = prime * result + this.monthOfYear;
		result = prime * result
				+ ((this.startTimeInMillisAfterMidnight == null) ? 0 : this.startTimeInMillisAfterMidnight.hashCode());
		result = prime * result + ((this.untilYear == null) ? 0 : this.untilYear.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		HolidayCalendarEntry other = (HolidayCalendarEntry) obj;
		if (this.asOfYear != other.asOfYear)
			return false;
		if (this.dayOfMonth != other.dayOfMonth)
			return false;
		if (this.endTimeInMillisAfterMidnight == null) {
			if (other.endTimeInMillisAfterMidnight != null)
				return false;
		} else if (!this.endTimeInMillisAfterMidnight.equals(other.endTimeInMillisAfterMidnight))
			return false;
		if (this.monthOfYear != other.monthOfYear)
			return false;
		if (this.startTimeInMillisAfterMidnight == null) {
			if (other.startTimeInMillisAfterMidnight != null)
				return false;
		} else if (!this.startTimeInMillisAfterMidnight.equals(other.startTimeInMillisAfterMidnight))
			return false;
		if (this.untilYear == null) {
			if (other.untilYear != null)
				return false;
		} else if (!this.untilYear.equals(other.untilYear))
			return false;
		return true;
	}

	/*****************************************************************************************
	 * Private methods
	 *****************************************************************************************/
}
